/*
 * Copyright the original author or authors.
 * 
 * Licensed under the MOZILLA PUBLIC LICENSE, Version 1.1 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.mozilla.org/MPL/MPL-1.1.html
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.kairos.motion
{
	import flash.events.Event;
	
	import com.bourre.commands.ASyncCommandListener;
	import com.kairos.engine.RTObject;
	import com.kairos.log.KairosDebug;
	import com.kairos.motion.strategy.LoopStrategy;
	import com.kairos.motion.strategy.NoLoopStrategy;
	import com.kairos.motion.strategy.PlayStrategy;
	import com.kairos.motion.strategy.ReversedLoopStrategy;
	import com.kairos.motion.strategy.ReversedNoLoopStrategy;	

	/**
	 * Abstract class for frame-based animations. In Kairos, frames are
	 * considered as a possible state of an animation, when the Flash
	 * Player actually consider each frames as a required way to perform
	 * the animation. Resulting in that animations could not be fully
	 * synchronized with the real time, if the frame rate fall, the real
	 * duration of the animation will be stretched.
	 * The <code>AbstractAnimation</code> provides a skeleton onto which
	 * you could create any type of frame based animations, such playing
	 * movie clip in real time, or creating bitmap based animations with
	 * spritesheets.
	 * <p>
	 * Animations are played according to the <code>RTBeacon</code> internal
	 * workflow. On each frame, the timestep is added to the internal time
	 * clock, the new frame gap is defined with the equation below :
	 * 
	 * <listing>timestep = rest + step;
	 * gap = ( timestep - ( timestep % framerate ) ) / framerate;</listing>
	 * 
	 * The new <code>rest</code> is calculated as follow : 
	 * 
	 * <listing>rest = timestep % framerate;</listing> 
	 * 
	 * If the gap is different than <code>0</code> (framerate of animation
	 * &gt;= framerate of the swf ), the playhead is moved. If the gap is
	 * greater than <code>1</code>, frames are skipped. But animation will
	 * stop immediatly if the looping parameter is set to <code>false</code>
	 * and if the playhead will go past total frames.
	 * </p><p>
	 * The <code>AbstractAnimation</code> use a <code>PlayStrategy</code>
	 * object to perform the playhead move, each time you change an animation
	 * paramater such <i>loop</i> or <i>reverse</i> the strategy is changed,
	 * in that way there is the minimum statements required to perform the
	 * animation. The <code>AbstractAnimation</code> provides 4 pre-defined
	 * strategies to covers all the cases below :
	 * <ul>
	 * <li><code>NO_LOOP</code> : Single animation in the normal time's arrow</li>	 * <li><code>REVERSED_NO_LOOP</code> : Single animation in the reversed time's arrow</li>	 * <li><code>LOOP</code> : Looping animation in the normal time's arrow</li>	 * <li><code>REVERSED_LOOP</code> : Looping animation in the reversed time's arrow</li>
	 * </ul>
	 * </p>
	 * @author Cédric Néhémie
	 */
	public class AbstractAnimation extends RTObject
	{
		/**
		 * Default value for animation speed.
		 */
		static public const DEFAULT_FRAMERATE : uint = 24;
		/**
		 * Looping animation strategy in the normal time's arrow.
		 */
		static protected const LOOP : PlayStrategy = new LoopStrategy();	
		/**
		 * Single animation strategy in the normal time's arrow.
		 */
		static protected const NO_LOOP : PlayStrategy = new NoLoopStrategy();	
		/**
		 * Looping animation strategy in the reversed time's arrow.
		 */
		static protected const REVERSED_LOOP : PlayStrategy = new ReversedLoopStrategy();	
		/**
		 * Single animation strategy in the reversed time's arrow.
		 */
		static protected const REVERSED_NO_LOOP : PlayStrategy = new ReversedNoLoopStrategy();
		
		/**
		 * The current frame number in the animation.
		 */
		protected var _nFrame : Number;
		/**
		 * The total number of frames in this animation.
		 */
		protected var _nTotalFrames : Number;
		/**
		 * The number of frames per second of this animation.
		 */
		protected var _nFrameRate : Number;
		/**
		 * The current <code>PlayStrategy</code> used by this animation.
		 */
		protected var _oStrategy : PlayStrategy;
		
		private var _bIsLooping : Boolean;
		private var _bIsReversed : Boolean;
		private var _nTime : Number;
		
		/**
		 * Initializes classical behaviors of an animation such speed, stop
		 * policy and time's direction. 
		 * <p>
		 * If <code>fps</code> argument is omitted then <code>fps</code> is set
		 * to the <code>DEFAULT_FRAMERATE</code>.
		 * </p><p>
		 * If <code>loop</code> argument is omitted then <code>loop</code> is set
		 * to <code>true</code>.
		 * </p><p>
		 * If <code>reverse</code> argument is omitted then <code>reverse</code>
		 * is set to <code>false</code>.
		 * </p> 
		 * @param   fps the speed of the animation in frame per second
		 * @param   l   <code>true</code> to make the motion looping, 
		 * 				<code>false</code> to make the motion stopping 
		 * 				at the end of the animation
		 * @param	r	<code>true</code> to make the animation playing in
		 * 				the reversed arrow of time  
		 */
		public function AbstractAnimation( fps : Number = DEFAULT_FRAMERATE, l : Boolean = true, r : Boolean = false )
		{
			setFramerate( fps );
			setReversed( r );
			setLooping( l );
		}
		
		/**
		 * Don't use, overwrite or override this method. That's the public
		 * callback of <code>RTBeacon</code>. The animation is performed during
		 * the call using the current <code>PlayStrategy</code> of this animation.
		 * 
		 * @param	e	event object dispatched by the beacon
		 */
		public final override function onTick ( e : Event = null ) : void
		{
			var t : Number = _nTime + getLocalStep( e );
			var f : Number =  ( t - ( _nTime = t % _nFrameRate ) ) / _nFrameRate;
			if( f ) _movePlayHead ( f );
		}
		
		/**
		 * Defines the speed in frames per seconds at which the animation is played.
		 * The defined framerate is effective only for this animation.  
		 * 
		 * @param   n	the number of frame per seconds.
		 * @throws 	<code>ArgumentError</code> — The passed in value is not a valid framerate value.
		 * @example <listing>mch.setFramerate ( 50 );</listing>
		 */
		public function setFramerate ( n : uint = DEFAULT_FRAMERATE ) : void
		{
			if( isNaN( n ) || n <= 0) 
				throw new ArgumentError ( "Number passed to " + this + ".setFramerate is not a valid value" );
			
			_nFrameRate = 1000 / n;
		}
		/**
		 * Returns the speed of the animation in frames per seconds.
		 * 
		 * @return  number of frames per seconds.
		 */
		public function getFramerate () : uint
		{
			return uint( 1000 / _nFrameRate );
		}
		
		/**
		 * Defines if the motion loop at the end of the animation.
		 *  
		 * @param	b	<code>true</code> to make the motion looping, 
		 * 				<code>false</code> to make the motion stopping 
		 * 				at the end of the animation
		 */
		public function setLooping ( b : Boolean ) : void
		{
			_bIsLooping = b;
			_switchStrategy ();
		}
		/**
		 * Returns <code>true</code> if the animtion loop, <code>false</code> otherwise.
		 * 
		 * @return <code>true</code> if the animtion loop, <code>false</code> otherwise
		 */
		public function isLooping () : Boolean
		{
		 	return _bIsLooping;
		}
		 
		/**
		 * Defines the way the animation plays. If <code>true</code>
		 * the animation play in the reversed time's direction.
		 * 
		 * @param	r	<code>true</code> to play the animation
		 * 				in the reversed time's direction, either
		 * 				<code>false</code> 
		 */
		public function setReversed ( r : Boolean ) : void
		{
			_bIsReversed = r;
			_switchStrategy ();
		}
		/**
		 * Returns <code>true</code> if the animation play in the
		 * reversed time's direction, either <code>false</code>.
		 * 
		 * @return 	<code>true</code> if the animation play in the
		 * 			reversed time's direction, either 
		 * 			<code>false</code>
		 */
		public function isReversed () : Boolean
		{
			return _bIsReversed;
		}
		
		/**
		 * Reverses the time's direction of the animation.
		 * <p>
		 * When animation is reversed and loop property is set
		 * to <code>false</code> the animation stop when arriving at the
		 * first frame.
		 * </p>
		 */
		public function reverse () : void
		{
			setReversed( !_bIsReversed );
		}
		
		/**
		 * Resets the animation state.
		 * <p>
		 * If the object is running the animation just restart
		 * from the first frame or the last frame according to
		 * the object play strategy.
		 * </p>
		 */
		override public function reset() : void
		{
		 	_nFrame = _oStrategy.getInitialFrame( _nTotalFrames );
			_nTime = 0;

			_frameChanged( _nFrame );
		}
		
		/**
		 * Uses the current strategy to produce animation.
		 * <p>
		 * Stop the animation if the strategy return <code>true</code>
		 * in <code>_isMotionFinished()</code>.
		 * </p>
		 */
		protected function _movePlayHead ( f : Number ) : void
		{
		 	if( _oStrategy.isMotionFinished ( _nFrame, _nTotalFrames ,f ) )
		 	{
		 		super.fireCommandEndEvent();
		 		super.stop(); 
		 	}
		 	_nFrame = _oStrategy.getNextFrame( _nFrame, _nTotalFrames, f );
		 	_frameChanged ( _nFrame );	 	
		} 
		
		/**
		 * Selects the play strategy according to all parameters of
		 * this animation. You can override that method to replace
		 * pre-defined strategies with your own strategies, or to
		 * extend the behavior of the animation with custom strategies.
		 * <p>
		 * When overriding this function, take care to clearly define
		 * which states will be available to the user. More formally,
		 * concret implementation can choose to not support reversed
		 * motion, or looping motion, in such cases, the class must
		 * throw an <code>UnsupportedOperationException</code> when
		 * calling methods which handle these states. If the concret
		 * implementation just wish to extend behaviors of the
		 * <code>AbstractAnimation</code> with new states, it should
		 * preserve all states defined by the <code>AbstractAnimation</code>
		 * class. 
		 * </p>
		 */
		protected function _switchStrategy () : void
		{
			if( _bIsLooping )
			{
				if( _bIsReversed ) _oStrategy = REVERSED_LOOP;
				else _oStrategy = LOOP;
			}
			else
			{
				if( _bIsReversed ) _oStrategy = REVERSED_NO_LOOP;
				else _oStrategy = NO_LOOP;
			}
		}
		
		/**
		 * Called when frame changed according to internal computation.
		 * Override this method to create the real frame change routine.
		 * 
		 * @param	f	the new frame for this animation
		 */
		protected function _frameChanged ( f : Number ) : void
		{}
		 
		/**
		 * Adds <code>ASyncCommandListener</code> as listener for the
		 * <code>onCommandEnd</code> event of the current object.
		 * <p>
		 * A warning log is send on the Kairos debug channel when you
		 * try to register a listener to an animation which have no end.
		 * </p>
		 * @param	listener <code>ASyncCommandListener</code> object to add
		 * 					 as listener for the <code>onCommandEnd</code> event
		 * 					 of the current object
		 * @param	rest	 any optional arguments defined by the
		 * 					 <code>EventBroadcaster.addEventListener</code> method
		 * @return	<code>true</code> if the object have been succesfully registered
		 * 			as listener for this object, either <code>false</code>
		 */
		public override function addASyncCommandListener( listener : ASyncCommandListener, ... rest ) : Boolean
		{
			if( _bIsLooping ) 
				KairosDebug.WARN( listener + " have registered to an object which will not fire the onCommandEnd event : " + this ); 
			
			return super.addASyncCommandListener.apply( null, [ listener ].concat ( rest ) );
		}
	}
}